Skip to content

add pinocchio spl-token-minter example#600

Open
MarkFeder wants to merge 5 commits into
solana-foundation:mainfrom
MarkFeder:tokens-spl-token-minter-pinocchio
Open

add pinocchio spl-token-minter example#600
MarkFeder wants to merge 5 commits into
solana-foundation:mainfrom
MarkFeder:tokens-spl-token-minter-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio implementation of tokens/spl-token-minter, continuing the Pinocchio token-examples lane after transfer-tokens (#596), escrow (#598), and create-token (#599).

What it does

Two instructions, matching the anchor and native options:

  • Create — creates an SPL Token mint (9 decimals) and attaches an on-chain Metaplex metadata account (name, symbol, URI).
  • Mint — mints tokens into the payer's associated token account, creating that account first if needed.

Notes

  • Instructions are dispatched by a leading discriminator byte (0 = Create, 1 = Mint), matching the Borsh enum variant index used by the native example.
  • The Metaplex Token Metadata program has no typed Pinocchio crate, so the CreateMetadataAccountV3 CPI is built by hand (raw InstructionView / cpi::invoke) — the same approach as add pinocchio create-token example #599.
  • The bankrun test loads the Metaplex program from a mainnet-dumped token_metadata.so fixture via a postinstall script, like the anchor example.
  • The mint authority is passed as a non-signer and aliased to the payer (which signs), mirroring the native example.

Files

  • New: tokens/spl-token-minter/pinocchio/ (program + bankrun test)
  • Edited: root Cargo.toml (workspace member), README.md (pinocchio link), Cargo.lock

@MarkFeder
MarkFeder force-pushed the tokens-spl-token-minter-pinocchio branch from 2819844 to 383287d Compare July 8, 2026 21:13
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio implementation of the tokens/spl-token-minter example, continuing the Pinocchio token-example series. The implementation is well-structured, closely mirrors the native example's wire format, and covers both instructions with a bankrun test that correctly validates on-chain state.

  • Create instruction (create.rs): allocates a mint account, calls InitializeMint2, and hand-rolls a CreateMetadataAccountV3 CPI against the mainnet-dumped Metaplex program; the Borsh layout is correctly serialized.
  • Mint instruction (mint.rs): uses CreateIdempotent to create the ATA if needed and then calls MintTo; the test verifies the resulting token balance using the official getTokenDecoder codec.
  • Infrastructure: prepare.mjs fetches the Metaplex fixture with solana program dump -um (per-command, no global config mutation); the deploy scripts replicate the same program.so filename convention used across all sibling pinocchio examples.

Confidence Score: 5/5

Safe to merge — all instruction logic, CPI construction, and test assertions are correct.

The program logic, Borsh serialization, CPI account layouts, and LiteSVM tests are all correct. The deploy scripts reference a mismatched filename, but this is a pre-existing pattern across sibling pinocchio examples and does not affect the build-and-test path used by CI.

The deploy script in package.json and cicd.sh reference program.so instead of the actual cargo output name, but this does not affect testing.

Important Files Changed

Filename Overview
tokens/spl-token-minter/pinocchio/program/src/instructions/create.rs Creates SPL mint and Metaplex metadata via hand-rolled CPI; Borsh layout is correct, signer aliasing (mint_authority == payer) is documented, and is_mutable:false is intentional.
tokens/spl-token-minter/pinocchio/program/src/instructions/mint.rs Clean MintTo + CreateIdempotent flow; non-signer mint_authority pattern is intentional and documented.
tokens/spl-token-minter/pinocchio/program/src/instructions/mod.rs Correct Borsh string parser with bounds-checked slice reads; parse_u64 is similarly guarded.
tokens/spl-token-minter/pinocchio/program/src/processor.rs Straightforward one-byte discriminator dispatch; returns InvalidInstructionData for empty data or unknown discriminators.
tokens/spl-token-minter/pinocchio/tests/test.ts LiteSVM tests validate mint account ownership, metadata account contents, and token balance using the official codec decoder — well structured and correct.
tokens/spl-token-minter/pinocchio/prepare.mjs Dumps Metaplex program from mainnet using per-command -um flag; no global config mutation; errors are non-fatal to surface as clear test failures.
tokens/spl-token-minter/pinocchio/package.json build-and-test script is correct; the deploy script references program.so which does not match cargo build-sbf output filename — same pre-existing pattern as sibling examples.
tokens/spl-token-minter/pinocchio/cicd.sh Deploy command references program.so but cargo will emit spl_token_minter_pinocchio_program.so — consistent with other pinocchio examples but would fail for learners following the deploy path.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Program as spl-token-minter<br/>(Pinocchio)
    participant System as System Program
    participant Token as SPL Token Program
    participant Metadata as Metaplex Token<br/>Metadata Program
    participant ATA as Associated Token<br/>Account Program

    Note over Client,ATA: Create instruction (discriminator = 0)
    Client->>Program: Create(name, symbol, uri)
    Program->>System: "CreateAccount(mint, 82 bytes, owner=Token)"
    Program->>Token: "InitializeMint2(decimals=9, mint_authority=payer)"
    Program->>Metadata: "CreateMetadataAccountV3(DataV2, is_mutable=false)"
    Metadata-->>Program: OK
    Program-->>Client: OK

    Note over Client,ATA: Mint instruction (discriminator = 1)
    Client->>Program: Mint(quantity)
    Program->>ATA: CreateIdempotent(payer, mint)
    ATA-->>Program: ATA created or already exists
    Program->>Token: "MintTo(mint, ata, mint_authority=payer, amount)"
    Token-->>Program: OK
    Program-->>Client: OK
Loading

Reviews (6): Last reviewed commit: "spl-token-minter pinocchio: use official..." | Re-trigger Greptile

try {
mkdirSync(outputDir, { recursive: true });
// Point the Solana CLI at mainnet, where the canonical program lives.
execSync("solana config set -um", { stdio: "inherit" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 postinstall mutates the global Solana CLI config

solana config set -um permanently sets the user's active cluster to mainnet-beta in ~/.config/solana/cli/config.yml. Any developer who runs pnpm install in this directory while working against devnet or localnet will silently have their Solana CLI endpoint changed. solana program dump accepts a --url flag (--url mainnet-beta or --url m), which would fetch from mainnet without touching the global config.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dd291b9. Switched to a per-command solana program dump -um <id> and dropped the global solana config set -um, so postinstall no longer mutates the developer's ~/.config/solana/cli/config.yml. This mirrors the pattern already used in the sibling pda-mint-authority example.

Comment on lines +46 to +52
MintTo {
mint: mint_account,
account: associated_token_account,
mint_authority,
amount: quantity,
}
.invoke()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Implicit mint_authority == payer invariant is not asserted

MintTo requires its mint_authority to be a signer. Here mint_authority (account index 1) is declared as a non-signer ([]) in the instruction — the CPI works only because the payer's address is passed for both slots and the payer signs the transaction. If a caller provides a genuinely different mint_authority key, the SPL Token program rejects the CPI with an opaque "missing required signature" error instead of a clear program-level message. The same applies to create.rs. For an educational example a quick equality check would make the design intent explicit and produce a cleaner error for learners who experiment with different account layouts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for this example. It models the canonical "payer is the mint authority" flow: the same key fills both the payer and mint_authority slots, and since the payer already signs the transaction the MintTo/InitializeMint CPIs satisfy the signer requirement — which is why mint_authority is marked non-signer ([]) here. The SPL Token program is the authority on that invariant and enforces it, so an added equality check would be redundant and would diverge from the anchor example this port mirrors. Happy to add an explicit assert if you'd prefer it for teaching clarity.

Comment on lines +53 to +56
function readTokenAmount(data: Uint8Array): number {
const buffer = Buffer.from(data);
return buffer.readUInt32LE(64) + buffer.readUInt32LE(68) * 4294967296; // 2^32
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 readTokenAmount silently loses precision for amounts above 2^53

The high-word multiplication buffer.readUInt32LE(68) * 4294967296 uses JavaScript's number type, which only holds integers exactly up to 2^53. Token amounts are stored as raw u64 units: 150 tokens at 9 decimals = 1.5 × 10^11, well under 2^53 and safe here. But if a future test mints a much larger amount, the assertion could silently pass with a wrong value. The u64le helper already acknowledges this ceiling; readTokenAmount should carry a similar comment, or the check should use BigInt arithmetic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dd291b9 via your suggested alternative — added a comment mirroring the u64le encoder, documenting the deliberate es6/no-BigInt design and the exact-below-2^53 guarantee (the 150-token mint asserted here is well within range). Kept number for symmetry with the encoder rather than introducing BigInt on only the decode side.

@MarkFeder
MarkFeder force-pushed the tokens-spl-token-minter-pinocchio branch from dd291b9 to c6690d5 Compare July 9, 2026 07:38
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@Perelyn-sama @dev-jodee — rebased onto latest main (picks up the ASM sbpf/Solana pin from #625), CI is now fully green. Ready for review whenever you have a chance 🙏

Move the async bankrun setup out of the `describe` callback and into a
`before` hook so Mocha collects the `it` blocks (an async `describe` body
registers tests after the suite is already collected, so nothing ran).

With the test now executing, replace `Rent::try_minimum_balance` with the
integer rent formula: its floating-point exemption-threshold path emits an
opcode the bankrun VM rejects ("unsupported BPF instruction"). Matches the
create-token example.
@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner July 15, 2026 21:20

@dev-jodee dev-jodee left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will require changes based on #624 final version

Apply the kit + litesvm template from the mint-close-authority example:
build and sign transactions with @solana/kit and run them on litesvm,
dropping @solana/web3.js and solana-bankrun entirely. PDAs (metadata, ATA)
are derived with getProgramDerivedAddress; the Metaplex Token Metadata
program is still dumped from mainnet by prepare.mjs and loaded into LiteSVM
via addProgramFromFile (litesvm bundles only the SPL programs).

- deps: drop @solana/web3.js + solana-bankrun, add litesvm; pin @solana/kit
  to ^6.10.0 (litesvm's kit major) so there is a single kit in the tree
- tsconfig: bump typescript to ^5 and lib to es2022+dom (kit's types), add
  @types/node; the suite is type-clean under tsc --noEmit
@MarkFeder
MarkFeder requested a review from dev-jodee July 21, 2026 23:10
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the kit + litesvm template from #624 here as well. The test now builds and signs transactions with @solana/kit and runs them on litesvm, so @solana/web3.js and solana-bankrun are dropped entirely. Pinned @solana/kit to ^6.10.0 (litesvm's kit major) for a single kit in the tree, and bumped typescript/tsconfig (es2022 + dom, @types/node) so the suite is type-clean under tsc --noEmit. Verified locally: cargo build-sbf + ts-mocha2 passing, plus tsc --noEmit and biome clean. Commit is signed.

Applies dev-jodee's solana-foundation#624 review refinements on top of the kit + litesvm test:

- program: compute rent with Rent::get()?.try_minimum_balance(MINT_SIZE)?
  instead of the integer-math workaround (only needed to dodge the f64 opcode
  the old bankrun VM rejected; litesvm runs the real syscall).
- test: source the token, associated-token and system program ids from the
  official @solana-program/token and @solana-program/system packages, and read
  the minted amount with getTokenDecoder().decode(...).amount instead of a raw
  byte offset. Token Metadata has no official @solana-program client, so its id
  stays hand-rolled.
- tsconfig: moduleResolution bundler for the packages' subpath exports.

Verified locally: cargo build-sbf + ts-mocha -> 2 passing, tsc --noEmit and
biome clean, frozen-lockfile OK.
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the same refinements @dev-jodee asked for on #624 (now merged):

  • program: rent is computed with Rent::get()?.try_minimum_balance(MINT_SIZE)? (the integer-math workaround was only needed to dodge the f64 opcode the old bankrun VM rejected; litesvm runs the real syscall).
  • test: the token, associated-token and system program ids now come from the official @solana-program/token / @solana-program/system packages, and the minted amount is read with getTokenDecoder().decode(...).amount instead of a raw byte offset. Token Metadata has no official @solana-program client, so its id stays hand-rolled.
  • tsconfig: moduleResolution: bundler for the packages' subpath exports.

cargo build-sbf + ts-mocha2 passing; tsc, biome, fmt and clippy clean; frozen-lockfile OK. Commit is signed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants